package squelette;


import javax.media.opengl.GL;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.glu.GLU;

import sceneGraph.SceneNode;
import sceneGraph.SceneHairySphereNode;
import sceneGraph.SceneTranslationNode;

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

import Lib3d.* ;

import java.nio.FloatBuffer ;



public class JavaRenderer implements GLEventListener, KeyListener, MouseListener, MouseMotionListener
{

   private static final GLU glu = new GLU();
   
   // Modifier ces parametres pour un rendu adapt� � la machine.
   
   public static final float TIME_RATIO = 0.5f;					// IMPORTANT, tout bouge super lentement si machine lente ! 0.5 sur PC normal (3Ghz + cg), 2.5 l'ISTIC. 
   
   public static final float TIME_SCALE = 0.015f;				// DT dans les calculs. Augmenter si PC lent !
   
   public static final float HAIRS_SPEED_RATIO = 1000.0f;				// Facteur vitesse sphere/vitesse poils. Sinon ca ne se voit pas assez...
   																		// Diminuer sur PC lent !!
   public static final float HAIRS_SPEED_RATIO_SPLINE_BALL = HAIRS_SPEED_RATIO;	// Idem, pour la spline ball.
   public static final float HAIRS_SPEED_RATIO_ROTATING_BALL = HAIRS_SPEED_RATIO;	// Idem, pour la spline ball.
   
   
   private static final float NEAR_CLIPPING = 1.0f;
   private static final float FAR_CLIPPING = 1000.0f;
   
   private float cameraX = 30.0f;
   private float cameraY = -30.0f;
   private float cameraZ = 50.0f;
   
   private float cameraAngleY = 0.0f;
   
   private static final float CAMERA_Y_VELOCITY = 50;						// Vitesse de déplacement caméra sur l'axe des Y.
   
   private static final float CAMERA_ANGLE_Y_STEP = 10f;					// Angle de rotation de caméra selon l'axe des Y.
   private static final float MOVE_FORWARD_VELOCITY = 5f;					// Vélocity d'avancée de le caméra, au clavier.
   
   private static final float CAMERA_ANGLE_Y_STEP_MOUSE_RATIO = 0.8f;		// Ratio vitesse souris/vélocité pour rotation caméra selon Y.
   private static final float MOVE_FORWARD_VELOCITY_MOUSE_RATIO = 0.8f;	// Ratio vitesse souris/velocité pour avancée caméra selin Y.
  

   private boolean isMouseClicked = false; 							// Utile pour le déplacement avec la souris.
   private int previousMousePositionX = 0;							// Stocke la position précédénte de la souris, pour le calcul de sa vitesse.
   private int previousMousePositionY = 0;
	
   private GL gl;

   // Noeuds de la scene.
   private SceneNode sceneRoot;
   private SceneNode sceneBouncingBallTranslation;
   private SceneNode sceneBouncingBall = new SceneHairySphereNode();
   
   private SceneNode sceneRotatingBallTranslation;
   private SceneNode sceneRotatingBall = new SceneHairySphereNode();
   
   private SceneNode sceneSplineBallTranslation;
   private SceneNode sceneSplineBall = new SceneHairySphereNode();
   
   // Bouncing ball
   private final float BOUNCING_BALL_SPEED_X = 0.1f * TIME_RATIO;
   private final float BOUNCING_BALL_SPEED_Y = 0.2f * TIME_RATIO;
   private final float BOUNCING_BALL_SPEED_Z = 0.5f * TIME_RATIO;
   
   private final float BOUNCING_BALL_MAX_X = 100;
   private final float BOUNCING_BALL_MIN_X = -BOUNCING_BALL_MAX_X;
   private final float BOUNCING_BALL_MAX_Y = 100;
   private final float BOUNCING_BALL_MIN_Y = -BOUNCING_BALL_MAX_Y;
   private final float BOUNCING_BALL_MAX_Z = -200;
   private final float BOUNCING_BALL_MIN_Z = -FAR_CLIPPING;
     
   private final float BOUNCING_BALL_INITIAL_X = (BOUNCING_BALL_MAX_X + BOUNCING_BALL_MIN_X) / 2;
   private final float BOUNCING_BALL_INITIAL_Y = (BOUNCING_BALL_MAX_Y + BOUNCING_BALL_MIN_Y) / 2;
   private final float BOUNCING_BALL_INITIAL_Z = (BOUNCING_BALL_MAX_Z + BOUNCING_BALL_MIN_Z) / 2;
   
   private Vector3d bouncingBallSpeed = new Vector3d(BOUNCING_BALL_SPEED_X, BOUNCING_BALL_SPEED_Y, BOUNCING_BALL_SPEED_Z);
   private Vector3d bouncingBallCoords = new Vector3d(BOUNCING_BALL_INITIAL_X, BOUNCING_BALL_INITIAL_Y, BOUNCING_BALL_INITIAL_Z);
   
   
   // Rotating ball
   private final float ROTATING_BALL_ROTATION_THETA_SPEED = 0.004f;	// AUGMENTER SI PC LENT
   private final float ROTATING_BALL_ROTATION_PHY_SPEED = 0.002f;
   
   private float rotatingBallAngleTheta = 0.0f;
   private float rotatingBallAnglePhy = 0.0f;
   private float ROTATING_BALL_EXCENTRICITY_RADIUS = 100;
   
   private Vector3d rotatingBallCoords = new Vector3d(0, 0, 0);

   
   // Spline ball
   private final Vector3d SPLINE_BALL_EXCENTRICITY = new Vector3d(0, 0, -300);
   
   private float splineBallCurrentTime = 0.0f;
   private final float SPLINE_BALL_TIME_ADVANCE_STEP = 0.001f * TIME_RATIO;
   
   private final Vector3d SPLINE_BALL_CRB1_P0 = new Vector3d(0, 0, 50);
   private final Vector3d SPLINE_BALL_CRB1_P1 = new Vector3d(100, 0, -40);
   private final Vector3d SPLINE_BALL_CRB1_P2 = new Vector3d(140, 140, 150);
   private final Vector3d SPLINE_BALL_CRB1_P3 = new Vector3d(0, 40, -80);
   
   private final Vector3d SPLINE_BALL_CRB2_P0 = SPLINE_BALL_CRB1_P3;
   private final Vector3d SPLINE_BALL_CRB2_P1 = Vector3d.add(SPLINE_BALL_CRB2_P0, Vector3d.neg(SPLINE_BALL_CRB1_P2));
   private final Vector3d SPLINE_BALL_CRB2_P2 = new Vector3d(100, -100, 50);
   private final Vector3d SPLINE_BALL_CRB2_P3 = new Vector3d(-40, 0, 150);
   
   private final Vector3d SPLINE_BALL_CRB3_P0 = SPLINE_BALL_CRB2_P3;
   private final Vector3d SPLINE_BALL_CRB3_P1 = Vector3d.add(SPLINE_BALL_CRB3_P0, Vector3d.neg(SPLINE_BALL_CRB2_P2));
   private final Vector3d SPLINE_BALL_CRB3_P2 = new Vector3d(-150, -300, 100);
   private final Vector3d SPLINE_BALL_CRB3_P3 = new Vector3d(-80, -60, 30);
   
   private final Vector3d SPLINE_BALL_CRB4_P0 = SPLINE_BALL_CRB3_P3;
   private final Vector3d SPLINE_BALL_CRB4_P1 = Vector3d.add(SPLINE_BALL_CRB4_P0, Vector3d.neg(SPLINE_BALL_CRB3_P2));
   private final Vector3d SPLINE_BALL_CRB4_P2 = new Vector3d(-100, 50, 100);
   private final Vector3d SPLINE_BALL_CRB4_P3 = new Vector3d(0, 0, 50);
   
   // Spline composée d'un multiple de 4 points.
   private final Vector3d[] splineCurves = new Vector3d[] {
		   SPLINE_BALL_CRB1_P0, SPLINE_BALL_CRB1_P1, SPLINE_BALL_CRB1_P2, SPLINE_BALL_CRB1_P3,
		   SPLINE_BALL_CRB2_P0, SPLINE_BALL_CRB2_P1, SPLINE_BALL_CRB2_P2, SPLINE_BALL_CRB2_P3,
		   SPLINE_BALL_CRB3_P0, SPLINE_BALL_CRB3_P1, SPLINE_BALL_CRB3_P2, SPLINE_BALL_CRB3_P3,
		   SPLINE_BALL_CRB4_P0, SPLINE_BALL_CRB4_P1, SPLINE_BALL_CRB4_P2, SPLINE_BALL_CRB4_P3,
		   };

   private int splineBallCurrentSpline = 0;
   
   private Vector3d splineBallCoords = new Vector3d(0, 0, 0);
 
   @Override
   public void display(GLAutoDrawable gLDrawable) 
   {
	   // display callback
       final GL gl = gLDrawable.getGL();
       
       gl.glClearColor(0.54f, 0.8f, 0.9f, 0.0f); // Couleur utilisée pour l'effacement du color buffer
       
       // clear color and depth buffer
       gl.glClear(GL.GL_COLOR_BUFFER_BIT);
       gl.glClear(GL.GL_DEPTH_BUFFER_BIT);
       
       // Transformations (equivalent à une translation de 300cm de la caméra suivant l'axe Z et une translation de 100cm suivant l'axe Y)
       gl.glLoadIdentity();
       
       //gl.glTranslatef(0.0f, -100.0f, -800.0f); 
       gl.glRotatef(-cameraAngleY, 0.0f, 1.0f, 0.0f) ; //Pour mieux visualser les axes 
       gl.glTranslatef(-cameraX, -cameraY, -cameraZ);
       
       // Affichage du repere du monde
       //Draw.drawAxis(gLDrawable, 400.0f);       
       
       // Bouncing Ball
       manageBouncingBall();
       ((SceneTranslationNode)sceneBouncingBallTranslation).setTranslation(bouncingBallCoords);
       
       // Rotating Ball
       manageRotatingBall();
       ((SceneTranslationNode)sceneRotatingBallTranslation).setTranslation(rotatingBallCoords);
       
       // Spline Ball;
       manageSplineBall();
       ((SceneTranslationNode)sceneSplineBallTranslation).setTranslation(splineBallCoords);
       
       // Affiche la scène.
       sceneRoot.draw(gLDrawable);
   }

   
   
   /**
    * Gère le mouvement de la Bouncing Ball.
    */
   private void manageBouncingBall() {
	   float vResX = 1; // Utilisés pour inverser les composantes vitesse si nécessaire.
	   float vResY = 1;
	   float vResZ = 1;
	   
	   // Teste les limites.
	   float x = bouncingBallCoords.get(0);
	   if ((x > BOUNCING_BALL_MAX_X) || (x < BOUNCING_BALL_MIN_X)) {
		   vResX = -1;
	   }

	   float y = bouncingBallCoords.get(1);
	   if ((y > BOUNCING_BALL_MAX_Y) || (y < BOUNCING_BALL_MIN_Y)) {
		   vResY = -1;
	   }

	   float z = bouncingBallCoords.get(2);
	   if ((z > BOUNCING_BALL_MAX_Z) || (z < BOUNCING_BALL_MIN_Z)) {
		   vResZ = -1;
	   }

	   bouncingBallSpeed = Vector3d.mul(bouncingBallSpeed, new Vector3d(vResX, vResY, vResZ));
	   bouncingBallCoords = Vector3d.add(bouncingBallCoords, bouncingBallSpeed);
	   
	   ((SceneHairySphereNode)sceneBouncingBall).setCurrentPosition(Vector3d.mul(bouncingBallCoords, HAIRS_SPEED_RATIO));
   }
   
   /**
    * Gère le mouvement de la Rotating Ball.
    */
   private void manageRotatingBall() {
	   rotatingBallAngleTheta = (rotatingBallAngleTheta + ROTATING_BALL_ROTATION_THETA_SPEED) % 180;
	   rotatingBallAnglePhy = (rotatingBallAnglePhy + ROTATING_BALL_ROTATION_PHY_SPEED) % 360;
	   rotatingBallCoords = getVectorFromSphericalPolarCoordinates(rotatingBallAngleTheta, rotatingBallAnglePhy, ROTATING_BALL_EXCENTRICITY_RADIUS);
	   
	   // Dans le calcul de vitesse, on prend en compte les coordonnées de la sphere autour de laquelle on tourne.
	   Vector3d rotatingBallCoordsAdded  = Vector3d.add(rotatingBallCoords, bouncingBallCoords);
	   ((SceneHairySphereNode)sceneRotatingBall).setCurrentPosition(Vector3d.mul(rotatingBallCoordsAdded, HAIRS_SPEED_RATIO_ROTATING_BALL));
   }

   /**
    * Calcule un vecteur à partir de coordonnées sphériques (2 angles et un rayon).
    * @param theta Angle 1
    * @param phy Angle 2
    * @param radius Rayon
    * @return Vecteur determiné à partir des angles et du rayon donnés.
    */
	public static Vector3d getVectorFromSphericalPolarCoordinates(float theta, float phy, float radius) {
		float x = (float)(radius * Math.sin(theta) * Math.cos(phy));
		float y = (float)(radius * Math.sin(theta) * Math.sin(phy));
		float z = (float)(radius * Math.cos(theta));
		return new Vector3d(x, y, z);
	}
	
   /**
    * Gère le mouvement de la Spline Ball.
    */
   private void manageSplineBall() {
	   
	   // Avance dans le "temps". Si on sort de l'echelle du temps, on passe à la spline suivante.
	   splineBallCurrentTime += SPLINE_BALL_TIME_ADVANCE_STEP;
	   if (splineBallCurrentTime > 1) {
		   splineBallCurrentTime = 0;
		   splineBallCurrentSpline = (splineBallCurrentSpline + 4) % splineCurves.length;
	   }

	   Vector3d vp0 = splineCurves[splineBallCurrentSpline];
	   Vector3d vp1 = splineCurves[splineBallCurrentSpline + 1];
	   Vector3d vp2 = splineCurves[splineBallCurrentSpline + 2];
	   Vector3d vp3 = splineCurves[splineBallCurrentSpline + 3];
	   
	   // B(u) = u^3(p3 + 3*(p1 - p2) - p0) + 3*u^2(p0 - 2*p1 + p2) + 3*u*(p1 - p0) + p0
	   // D'apres http://www.paultondeur.com/2008/03/09/drawing-a-cubic-bezier-curve-using-actionscript-3/
	   float x = calculateSpline(0, splineBallCurrentTime, vp0, vp1, vp2, vp3);
	   float y = calculateSpline(1, splineBallCurrentTime, vp0, vp1, vp2, vp3);
	   float z = calculateSpline(2, splineBallCurrentTime, vp0, vp1, vp2, vp3);

	   splineBallCoords = Vector3d.add(new Vector3d(x, y, z), SPLINE_BALL_EXCENTRICITY);
	   
	   ((SceneHairySphereNode)sceneSplineBall).setCurrentPosition(Vector3d.mul(splineBallCoords, HAIRS_SPEED_RATIO_SPLINE_BALL));
   }
   
   /**
    * Calcule la coordonnée d'un point sur la trajectoire d'une Spline.
    * @param dimension Dimension dont on calcule le point dans la Spline.
    * @param u Unité de temps (de 0.0 à 1.0 inclus).
    * @param vp0 Vecteur du 1er point de la spline.
    * @param vp1 Vecteur du 2e point de la spline.
    * @param vp2 Vecteur du 3e point de la spline.
    * @param vp3 Vecteur du 4e point de la spline.
    * @return Coordonnée du point sur la trajectore, de dimension donnée en parametre.
    */
   private float calculateSpline(int dimension, float u, Vector3d vp0, Vector3d vp1, Vector3d vp2, Vector3d vp3) {
	   float p0 = vp0.get(dimension);
	   float p1 = vp1.get(dimension);
	   float p2 = vp2.get(dimension);
	   float p3 = vp3.get(dimension);
	   
	   // u^3(p3 + 3*(p1 - p2) - p0)
	   double res = Math.pow(u, 3) * (p3 + 3 * (p1 - p2) - p0);
	   
	   // + 3*u^2(p0 - 2*p1 + p2)
	   res += 3 * Math.pow(u, 2) * (p0 - 2 * p1 + p2);
	   
	   // + 3*u*(p1 - p0) + p0
	   res += 3 * u * (p1 - p0) + p0;
	   
	   return (float)res;
   }
   
   
   

   public void displayChanged(GLAutoDrawable gLDrawable, boolean modeChanged, boolean deviceChanged) 
   {
   }

   public void init(GLAutoDrawable gLDrawable) 
   {	   
	   gl = gLDrawable.getGL();

	   SceneHairySphereNode.gl = gLDrawable.getGL();
	   
	   // Créé le graph de scène.
	   sceneRoot = new SceneNode();
	   
	   // Ajoute l'outline.
	   SceneNode sceneOutline = new sceneGraph.SceneOutlineNode();
	   sceneRoot.addNode(sceneOutline);
	   
	   // Bouncing Ball
	   // -------------
	   // Composée d'un noeud TR, lui même composé d'une sphere.
	   // Le tout est lié au noeud Root.
	   
	   // Translation
	   sceneBouncingBallTranslation = new SceneTranslationNode(bouncingBallCoords);
	   sceneRoot.addNode(sceneBouncingBallTranslation);
	   
	   // Sphere
	   sceneBouncingBallTranslation.addNode(sceneBouncingBall);
	  
	   
	   // Rotating Ball
	   // -------------
	   // Composée d'un noeud TR, puis d'une sphere.
	   // Le tout est lié au noeud SceneBouncingBallTR.
	   
	   // Translation
	   sceneRotatingBallTranslation = new SceneTranslationNode(new Vector3d(0, 0, 0));
	   
	   // Sphere
	   sceneRotatingBallTranslation.addNode(sceneRotatingBall);
	   
	   // Attache à la Bouncing Ball.
	   sceneBouncingBallTranslation.addNode(sceneRotatingBallTranslation);
	   
	   
	   
	   
	   // Spline Ball
	   // -----------
	   // Translation
	   sceneSplineBallTranslation = new SceneTranslationNode(splineBallCoords);
	   sceneRoot.addNode(sceneSplineBallTranslation);
	   
	   // Sphere
	   sceneSplineBallTranslation.addNode(sceneSplineBall);
	   
	   
	  
	   
	   //-----------------------
	   // Initialisations OpenGL
	   //-----------------------

	   // init callback
       gl.glShadeModel(GL.GL_SMOOTH); // Mode de shading
       gl.glClearColor(1.0f, 1.0f, 1.0f, 0.0f); // Couleur utilis�e pour l'effacement du color buffer
       // init z-buffer
       gl.glClearDepth(1.0f);
       gl.glEnable(GL.GL_DEPTH_TEST);
       gl.glDepthFunc(GL.GL_LEQUAL);
       gl.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);
       // Autorise l'utilisation des textures
       //gl.glEnable(GL.GL_TEXTURE_2D);     
       // Autorise l'utilisation des lumi�res
       gl.glEnable(GL.GL_LIGHTING) ;
       // Active la lumi�re 0
       gl.glEnable(GL.GL_LIGHT0);
       //float[] lightColor = {0.4f, 0.4f, 0.4f} ;	// Couleur de la lumi�re
       float[] lightColor = {0.0f, 0.0f, 1.0f} ;	// Couleur de la lumi�re
       float[] lightPosition = {0.0f, 0.0f, 0.0f} ; // Position de la lumi�re
       
       // Initialisation des composantes colorim�triques de la lumi�re
       gl.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, FloatBuffer.wrap(lightColor)) ;
       gl.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, FloatBuffer.wrap(lightColor)) ;
       gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPECULAR, FloatBuffer.wrap(lightColor)) ;
       // Positionnement de la lumi�re dans l'environnement
       gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, FloatBuffer.wrap(lightPosition)) ;
       // listener clavier
       gLDrawable.addKeyListener(this);
       gLDrawable.addMouseListener(this);
       gLDrawable.addMouseMotionListener(this);
   }

   public void reshape(GLAutoDrawable gLDrawable, int x, int y, int width, int height) 
   {
       final GL gl = gLDrawable.getGL();
       if(height <= 0) { height = 1; }
       // camera and perspective
       final float h = (float)width / (float)height;
       gl.glMatrixMode(GL.GL_PROJECTION);
       gl.glLoadIdentity();
       glu.gluPerspective(50.0f, h, NEAR_CLIPPING, FAR_CLIPPING);
       gl.glMatrixMode(GL.GL_MODELVIEW);
       gl.glLoadIdentity();
   }

   public void keyPressed(KeyEvent e) {
	   // keypressed Event
       if(e.getKeyCode() == KeyEvent.VK_ESCAPE) 
       {
           JavaMain.bQuit = true;
           //JavaDia.displayT.stop();
           JavaMain.displayT.interrupt() ;
           System.exit(0);
       } else if (e.getKeyCode() == KeyEvent.VK_F) {
    	   cameraY -= CAMERA_Y_VELOCITY;
       } else if (e.getKeyCode() == KeyEvent.VK_R) {
    	   cameraY += CAMERA_Y_VELOCITY;
       } else if (e.getKeyCode() == KeyEvent.VK_Z) {
    	   avancerCameraXZ(MOVE_FORWARD_VELOCITY);
       } else if (e.getKeyCode() == KeyEvent.VK_S) {
    	   avancerCameraXZ(-MOVE_FORWARD_VELOCITY);
       } else if (e.getKeyCode() == KeyEvent.VK_Q) {
    	   cameraAngleY += CAMERA_ANGLE_Y_STEP;
       } else if (e.getKeyCode() == KeyEvent.VK_D) {
    	   cameraAngleY -= CAMERA_ANGLE_Y_STEP;
       } 

   }
   
   /**
    * Avance la caméra.
    * @param enAvant True pour aller "en avant".
    */
   private void avancerCameraXZ(float velocity) {
	   double angleYRadian = -cameraAngleY/180.0*Math.PI;
	   
	   // Notre vecteur de rotation est dirigé vers Z uniquement. La composante en X est donc nulle.
	   float shiftX = (float)((-Math.sin(angleYRadian)*-MOVE_FORWARD_VELOCITY));
	   float shiftZ = (float)((Math.cos(angleYRadian)*-MOVE_FORWARD_VELOCITY));
	   
	   cameraX += velocity * shiftX;
	   cameraZ += velocity * shiftZ;
   }
   
   



	
	@Override
	public void mouseDragged(MouseEvent e) {
		if (!isMouseClicked) {
			isMouseClicked = true;
			previousMousePositionX = e.getX();
			previousMousePositionY = e.getY();
		} else {
			int speedX = e.getX() - previousMousePositionX;
			int speedY = e.getY() - previousMousePositionY;
			previousMousePositionX = e.getX();
			previousMousePositionY = e.getY();
			
			if (speedY != 0) {
				avancerCameraXZ(-MOVE_FORWARD_VELOCITY_MOUSE_RATIO * speedY);
			}
			
			if (speedX != 0) {
				cameraAngleY -= CAMERA_ANGLE_Y_STEP_MOUSE_RATIO * speedX;
			}
			
			
		}
	}

	@Override
	public void mouseReleased(MouseEvent arg0) {
		isMouseClicked = false;
	}
	
	
	@Override
	public void mouseMoved(MouseEvent e) { }

	@Override
	public void mouseClicked(MouseEvent arg0) { }

	@Override
	public void mouseEntered(MouseEvent arg0) {	}

	@Override
	public void mouseExited(MouseEvent arg0) { }

	@Override
	public void mousePressed(MouseEvent arg0) {	}

	@Override
	public void keyReleased(KeyEvent e) { }

	@Override
	public void keyTyped(KeyEvent e) { }


}
